package com.hospital.rank.module.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hospital.common.base.Result;
import com.hospital.common.dto.rank.module.RankSearchDto;
import com.hospital.common.enums.ResultStatus;
import com.hospital.common.enums.post.module.PinnedFlag;
import com.hospital.common.pojo.TPostMeta;
import com.hospital.common.pojo.TRank;
import com.hospital.common.pojo.TSection;
import com.hospital.common.pojo.TUser;
import com.hospital.common.utils.CalculateUtils;
import com.hospital.common.utils.LocalDateTimeUtils;
import com.hospital.common.vo.management.module.DoctorInfoVo;
import com.hospital.common.vo.rank.module.DoctorRankVo;
import com.hospital.common.vo.rank.module.PostRankVo;
import com.hospital.common.vo.rank.module.SectionRankVo;
import com.hospital.common.vo.section.module.SectionVo;
import com.hospital.rank.module.base.PeriodType;
import com.hospital.rank.module.base.TargetType;
import com.hospital.rank.module.entity.DoctorStatEntity;
import com.hospital.rank.module.entity.PostStatEntity;
import com.hospital.rank.module.entity.SectionStatEntity;
import com.hospital.rank.module.feign.ManagementModuleFeign;
import com.hospital.rank.module.feign.SectionModuleFeign;
import com.hospital.rank.module.mapper.TPostMetaMapper;
import com.hospital.rank.module.mapper.TRankMapper;
import com.hospital.rank.module.mapper.TSectionMapper;
import com.hospital.rank.module.mapper.TUserMapper;
import com.hospital.rank.module.service.TRankService;
import com.hospital.rank.module.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.hospital.common.base.interfaces.Const.HOT_POST_KEY;
import static com.hospital.common.base.interfaces.Const.SECTION_RANK_KEY;

@Service
public class TRankServiceImpl extends ServiceImpl<TRankMapper, TRank> implements TRankService {
    @Autowired
    private TPostMetaMapper postMetaMapper;
    @Autowired
    private TRankMapper rankMapper;
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private TSectionMapper sectionMapper;
    @Autowired
    private TUserMapper userMapper;
    @Autowired
    private ManagementModuleFeign managementModuleFeign;
    @Autowired
    private SectionModuleFeign sectionModuleFeign;

    @Override
    public void rankStat(PeriodType periodType, TargetType targetType) {
        switch (targetType) {
            case DOCTOR -> doctorRankStat(periodType);
            case SECTION -> sectionRankStat(periodType);
            case POST -> postRankStat(periodType);
        }
    }

    @Override
    public List<?> rankSearch(TargetType targetType, PeriodType periodType) {
        String key = "rank:" + targetType.value + ":" + periodType.value;
        if (!redisUtils.hasKey(key)) {
            return Collections.emptyList();
        }
        Map<Object, Object> topN = redisUtils.getTopN(key, 10);
        if(topN.isEmpty()){
            return Collections.emptyList();
        }
        switch (targetType){
            case DOCTOR -> {
                return convertDoctorRankVo(topN);
            }
            case SECTION -> {
                List<SectionRankVo> sectionRankVos = new ArrayList<>();
                for (Map.Entry<Object, Object> entry : topN.entrySet()){
                    SectionRankVo sectionRankVo = new SectionRankVo();
                    TSection query = sectionMapper.selectById(((Number)entry.getKey()).longValue());
                    sectionRankVo.setSectionName(ObjectUtils.isEmpty( query)? "未知板块" : query.getName());
                    sectionRankVo.setScore(((Number)entry.getValue()).longValue());
                    Result<SectionVo> section = sectionModuleFeign.findSectionById(((Number)entry.getKey()).longValue());
                    if(section.getCode().equals(ResultStatus.SUCCESS.getCode())){
                        sectionRankVo.setSectionInfo(section.getData());
                    }
                    sectionRankVos.add(sectionRankVo);
                }
                return sectionRankVos;
            }
            case POST -> {
                return convertPostRankVo(topN);
            }
            default -> {
                return Collections.emptyList();
            }
        }
    }

    @Override
    public List<PostRankVo> hotPostSearch() {
        if (!redisUtils.hasKey(HOT_POST_KEY)) {
            return Collections.emptyList();
        }
        Map<Object, Object> topN = redisUtils.getTopN(HOT_POST_KEY, 20);
        if(topN.isEmpty()){
            return Collections.emptyList();
        }
        return convertPostRankVo(topN);
    }

    @Override
    public void hotPostStat() {
        List<PostStatEntity> postStatEntities = postMetaMapper.statHotPost();
        if (ObjectUtils.isEmpty(postStatEntities)) {
            return;
        }
        Map<Long, Long> map = postStatEntities.stream()
                .collect(Collectors.toMap(PostStatEntity::getTargetId, v -> CalculateUtils.calculatePostScore(
                        v.getHeatScore(),
                        v.getIsPinned().equals(PinnedFlag.PINNED.value)
                )));
        saveInDB(postStatEntities.stream().map(PostStatEntity::getTargetId).toList(), TargetType.HOT_POST, null, LocalDateTime.now());
        saveInRedis(HOT_POST_KEY, map);
    }
    @Override
    public List<?> rankSearchInSection(RankSearchDto rankSearchDto) {
        if(ObjectUtils.isEmpty(rankSearchDto.getSectionId())){
            return Collections.emptyList();
        }
        String key = SECTION_RANK_KEY + rankSearchDto.getTargetType() + ":" + rankSearchDto.getSectionId() + ":" + rankSearchDto.getPeriodType();
        if (!redisUtils.hasKey(key)) {
            return Collections.emptyList();
        }
        Map<Object, Object> topN = redisUtils.getTopN(key, 10);
        if(topN.isEmpty()){
            return Collections.emptyList();
        }
        TargetType targetType = TargetType.getTargetType(rankSearchDto.getTargetType());
        switch (targetType){
            case POST -> {
                return convertPostRankVo(topN);
            }
            case DOCTOR -> {
                return convertDoctorRankVo(topN);
            }
        }
        return Collections.emptyList();
    }

    private List<DoctorRankVo> convertDoctorRankVo(Map<Object, Object> topN) {
        List<DoctorRankVo> doctorRankVos = new ArrayList<>();
        for (Map.Entry<Object, Object> entry : topN.entrySet()){
            Result<DoctorInfoVo> doctor = managementModuleFeign.getDoctor(((Number)entry.getKey()).longValue());
            String photoUrl = null;
            if(doctor.getCode().equals(ResultStatus.SUCCESS.getCode())){
                DoctorInfoVo doctorInfoVo = doctor.getData();
                if(!ObjectUtils.isEmpty(doctorInfoVo)){
                    photoUrl = doctorInfoVo.getMinioPath();
                }
            }
            DoctorRankVo doctorRankVo = new DoctorRankVo();
            TUser user = userMapper.selectById(((Number)entry.getKey()).longValue());
            doctorRankVo.setName(ObjectUtils.isEmpty(user)?"未知医生":user.getName());
            doctorRankVo.setScore(((Number)entry.getValue()).longValue());
            doctorRankVo.setPhotoUrl(photoUrl);
            doctorRankVos.add(doctorRankVo);
        }
        return doctorRankVos;
    }

    @Override
    public void rankStatInSection(PeriodType periodType, TargetType targetType) {
        LambdaQueryWrapper<TSection> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.select(TSection::getId);
        List<TSection> sections = sectionMapper.selectList(queryWrapper);
        if(ObjectUtils.isEmpty(sections)){
            return;
        }
        switch (targetType){
            case DOCTOR -> {
                postRankStatInSection(periodType,sections);
            }
            case POST -> {
                doctorRankStatInSection(periodType,sections);
            }
        }
    }
    private void postRankStatInSection(PeriodType periodType,List<TSection> sections){
        LocalDateTime startTime = null;
        LocalDateTime endTime = LocalDateTimeUtils.now();
        List<PostStatEntity> postStatEntities = new ArrayList<>();
        for (TSection section : sections){
            StringBuilder key = new StringBuilder(SECTION_RANK_KEY+TargetType.POST.value+":");
            key.append(section.getId()).append(":");
            switch (periodType){
                case WEEKLY -> {
                    startTime = LocalDateTimeUtils.startWeeksAgo(1);
                    postStatEntities = postMetaMapper.statPostBySectionId(section.getId(), startTime, endTime);
                    key.append(periodType.value);
                }
                case MONTHLY -> {
                    startTime = LocalDateTimeUtils.startMonthsAgo(1);
                    postStatEntities = postMetaMapper.statPostBySectionId(section.getId(), startTime, endTime);
                    key.append(periodType.value);
                }
            }
            if(ObjectUtils.isEmpty(postStatEntities)){
                return;
            }
            Map<Long, Long> map = postStatEntities.stream()
                    .collect(Collectors.toMap(PostStatEntity::getTargetId, v -> CalculateUtils.calculatePostScore(
                            v.getHeatScore(),
                            v.getIsPinned().equals(PinnedFlag.PINNED.value)
                    )));
            saveInRedis(key.toString(), map);
        }
    }
    private void doctorRankStatInSection(PeriodType periodType, List<TSection> sections) {
        LocalDateTime startTime = null;
        LocalDateTime endTime = LocalDateTimeUtils.now();
        List<DoctorStatEntity> doctorStatEntities = new ArrayList<>();
        for (TSection section : sections){
            StringBuilder key = new StringBuilder(SECTION_RANK_KEY+ TargetType.DOCTOR.value+":");
            key.append(section.getId()).append(":");
            switch (periodType){
                case WEEKLY -> {
                    startTime = LocalDateTimeUtils.startWeeksAgo(1);
                    doctorStatEntities = postMetaMapper.statDoctorPostBySectionId(section.getId(), startTime, endTime);
                    key.append(periodType.value);
                }
                case MONTHLY -> {
                    startTime = LocalDateTimeUtils.startMonthsAgo(1);
                    doctorStatEntities = postMetaMapper.statDoctorPostBySectionId(section.getId(), startTime, endTime);
                    key.append(periodType.value);
                }
            }
            if(ObjectUtils.isEmpty(doctorStatEntities)){
                return;
            }
            common(doctorStatEntities, key.toString());
        }
    }

    private List<PostRankVo> convertPostRankVo(Map<Object, Object> topN){
        List<PostRankVo> postRankVos = new ArrayList<>();
        for (Map.Entry<Object, Object> entry : topN.entrySet()){
            PostRankVo postRankVo = new PostRankVo();
            TPostMeta postMeta = postMetaMapper.selectById(((Number)entry.getKey()).longValue());
            if(ObjectUtils.isEmpty(postMeta)){
                continue;
            }
            postRankVo.setTitle(postMeta.getTitle());
            TUser user = userMapper.selectById(postMeta.getAuthorId());
            postRankVo.setAuthor(ObjectUtils.isEmpty(user)? "未知作者" : user.getName());
            postRankVo.setScore(((Number)entry.getValue()).longValue());
            postRankVo.setPostMeta(postMeta);
            postRankVos.add(postRankVo);
        }
        return postRankVos;
    }
    private void doctorRankStat(PeriodType periodType) {
        List<DoctorStatEntity> doctorStatEntities = new ArrayList<>();
        String key = "rank:" + TargetType.DOCTOR.value + ":";
        LocalDateTime endTime = LocalDateTimeUtils.now();
        LocalDateTime startTime = null;
        switch (periodType) {
            case WEEKLY -> {
                startTime = LocalDateTimeUtils.startWeeksAgo(1);
                doctorStatEntities = postMetaMapper.statDoctorPost(startTime, endTime);
                key += periodType.value;
            }
            case MONTHLY -> {
                startTime = LocalDateTimeUtils.startMonthsAgo(1);
                doctorStatEntities = postMetaMapper.statDoctorPost(startTime, endTime);
                key += periodType.value;
            }
        }
        if (ObjectUtils.isEmpty(doctorStatEntities)) {
            return;
        }
        common(doctorStatEntities, key);
        saveInDB(doctorStatEntities.stream().map(DoctorStatEntity::getTargetId).toList(),TargetType.DOCTOR, startTime, endTime);
    }
    private void common(List<DoctorStatEntity> doctorStatEntities, String key){
        Map<Long, Long> map = doctorStatEntities.stream()
                .collect(Collectors.toMap(DoctorStatEntity::getTargetId, v -> CalculateUtils.calculateDoctorScore(
                        CalculateUtils.calculateAverageHeatScore(v.getTotalHeatScore(), v.getTotalPostCount()),
                        CalculateUtils.calculateDoctorPostLikeRatio(v.getTotalLikeCount(), v.getTotalPostCount()),
                        CalculateUtils.calculateDoctorPostPinnedRatio(v.getPinnedPostCount(), v.getTotalPostCount())
                ), Long::sum));
        saveInRedis(key, map);
    }

    private void sectionRankStat(PeriodType periodType) {
        List<SectionStatEntity> sectionStatEntities = new ArrayList<>();
        String key = "rank:" + TargetType.SECTION.value + ":";
        LocalDateTime endTime = LocalDateTimeUtils.now();
        LocalDateTime startTime = null;
        switch (periodType) {
            case WEEKLY -> {
                startTime = LocalDateTimeUtils.startWeeksAgo(1);
                sectionStatEntities = postMetaMapper.statSection(startTime, endTime);
                key += periodType.value;
            }
            case MONTHLY -> {
                startTime = LocalDateTimeUtils.startMonthsAgo(1);
                sectionStatEntities = postMetaMapper.statSection(startTime, endTime);
                key += periodType.value;
            }
        }
        if (ObjectUtils.isEmpty(sectionStatEntities)) {
            return;
        }
        Map<Long, Long> map = sectionStatEntities.stream()
                .collect(Collectors.toMap(SectionStatEntity::getTargetId, v -> CalculateUtils.calculateSectionScore(
                        CalculateUtils.calculateSectionQualityRatio(
                            CalculateUtils.calculateSectionPostLikeRatio(v.getTotalLikeCount(), v.getTotalPostCount()),
                            CalculateUtils.calculateSectionParticipation(v.getTotalReplyCount(), v.getTotalLikeCount(), v.getTotalFavoriteCount()).doubleValue()
                        ),
                        CalculateUtils.calculateSectionParticipation(v.getTotalReplyCount(), v.getTotalLikeCount(), v.getTotalFavoriteCount())
                )));
        saveInDB(sectionStatEntities.stream().map(SectionStatEntity::getTargetId).toList(),TargetType.SECTION, startTime, endTime);
        saveInRedis(key, map);
    }
    private void postRankStat(PeriodType periodType) {
        List<PostStatEntity> postStatEntities = new ArrayList<>();
        String key = "rank:" + TargetType.POST.value + ":";
        LocalDateTime endTime = LocalDateTimeUtils.now();
        LocalDateTime startTime = null;
        switch (periodType) {
            case WEEKLY -> {
                startTime = LocalDateTimeUtils.startWeeksAgo(1);
                postStatEntities = postMetaMapper.statPost(startTime, endTime);
                key += periodType.value;
            }
            case MONTHLY -> {
                startTime = LocalDateTimeUtils.startMonthsAgo(1);
                postStatEntities = postMetaMapper.statPost(startTime, endTime);
                key += periodType.value;
            }
        }
        if (ObjectUtils.isEmpty(postStatEntities)) {
            return;
        }
        Map<Long, Long> map = postStatEntities.stream()
                .collect(Collectors.toMap(PostStatEntity::getTargetId, v -> CalculateUtils.calculatePostScore(
                        v.getHeatScore(),
                        v.getIsPinned().equals(PinnedFlag.PINNED.value)
                )));
        saveInDB(postStatEntities.stream().map(PostStatEntity::getTargetId).toList(),TargetType.POST, startTime, endTime);
        saveInRedis(key, map);
    }

    private void saveInRedis(String key, Map<Long, Long> map) {
        redisUtils.del(key);
        for (Map.Entry<Long, Long> entry : map.entrySet()){
            if(!ObjectUtils.isEmpty(entry.getKey())){
                redisUtils.zAdd(key, entry.getKey(), entry.getValue());
            }
        }
    }
    private void saveInDB(List<Long> targetIds, TargetType targetType, LocalDateTime startTime, LocalDateTime endTime){
        for (Long targetId : targetIds){
            TRank rank = new TRank();
            rank.setTargetId(targetId);
            rank.setTargetType(targetType.value);
            rank.setPeriodStartTime(startTime);
            rank.setPeriodEndTime(endTime);
            rank.setCreateTime(LocalDateTime.now());
            rankMapper.insert(rank);
        }
    }
}
